今天我們要來介紹Angular Material中最複雜的元件之一:表格Table。透過組合table、sort header和paginator這三個功能,我們會完成一個大部分情境都適用的data table。
Data table可以說是許多軟體都會被使用到的功能,尤其是管理各種資料的後台程式,更是使用data table的大宗來源,而在商務應用上後台軟體的開發需求也是源源不絕,因此data table可以說是前端應用最大的一個議題也不為過!
也因此在Angular Material中要設計data table自然有非常多彈性可以調整的地方,尤其是我們會一次組合3種元件,來完成data table的功能,讓狀況更加的複雜,因此我們會將data table這個主題拆成2篇介紹,今天我們會先完成一個大部分情境都適用的data table,明天則會針對一些細節的部分做進階的介紹;準備好了嗎?開始囉!
在Material Design的Data tables設計指南中,data table用來呈現多筆的資料列,在許多系統中的會使用到,我們能透過data table呈現資料,也能夠進行資料的管理。
Data table基本上就是表格的呈現,只是比起傳統HTML的表格,應該具備更多的功能,如分頁、排序等等。
基本上大多數的data table,都需要3個主要部分:資料顯示的主體、允許排序的標題及分頁。在Angular Material中這三個功能分別放在MatTableModule
、MatSortModule
及MatPaginatorModule
,我們今天會一口氣把這三個功能都介紹過,來完成一個基本功能完整的data table,因此我們可以先把這3個module都加到我們的前端專案中。
我們先從最基本的顯示資料主體開始,使用到<mat-table>
這個元件;在Angular Material中,使用<mat-table>
與一般table的使用方式會略有不同,因此讓我們一步一步的來完成
我們先把資料來源準備好
@Component({ })
export class InboxComponent implements OnInit {
emailsDataSource = new MatTableDataSource<any>();
constructor(private httpClient: HttpClient) {}
ngOnInit() {
this.httpClient.get<any>('https://api.github.com/search/issues?q=repo:angular/material2&page=1').subscribe(data => {
this.emailsDataSource.data = data.items;
});
}
}
這裡我們先把Angular Material的GitHub repository中的issues當作是我們的email資料來源,同時我們建立一個emailDataSource
當作資料的來源,他的型別是MatTableDataSource<T>
,其中的data: T
屬性是用來放置主要呈現資料的屬性,其他屬性我們之後的內容慢慢理解。
資料來源API:https://api.github.com/search/issues?q=repo:angular/material2&page=1
接著在畫面上可以使用一個<mat-table>
當作data table的主體,同時使用dataSource
屬性設定資料的來源,來源必須是MatTableDataSource<T>
,也就是我們在程式中的emailDataSource
:
<mat-table [dataSource]="emailsDataSource">
</mat-table>
接著在<mat-table>
裡面,我們可以使用<ng-container matColumnDef="xxxx">
來定義一個表格的欄位(column),matColumnDef="xxxx"
代表這個欄位的名稱。
而在欄位裡面需要提供兩個資訊:
<mat-header-cell *matHeaderCellDef>
:代表資料在標題列cell內容。<mat-cell *matCellDef="let xxx">
:代表實際呈現資料的cell。在這裡我們定義了4個column,其中3個負責呈現資料,最後一個則是用來管理資料用的:
<mat-table [dataSource]="emailsDataSource">
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef>寄件人</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user.login }}</mat-cell>
</ng-container>
<ng-container matColumnDef="title">
<mat-header-cell *matHeaderCellDef>標題</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.title }}</mat-cell>
</ng-container>
<ng-container matColumnDef="created_at">
<mat-header-cell *matHeaderCellDef>日期</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.created_at }}</mat-cell>
</ng-container>
<ng-container matColumnDef="management">
<mat-header-cell *matHeaderCellDef>
<u>管理</u>
</mat-header-cell>
<mat-cell *matCellDef="let row">
<button mat-button color="primary" (click)="reply(row)">回覆</button>
<button mat-button color="warn" (click)="delete(row)">刪除</button>
</mat-cell>
</ng-container>
</mat-table>
我們可以使用<mat-header-row *matHeaderRowDef="[]">
語法,將資料的標題列顯示出來,其中*matHeaderRowDef="[]"
放置的是每個標題欄位的名稱,也就是我們上個步驟的matColumnDef="xxx"
的名稱,設定後會把對應名稱column裡的<mat-header-cell>
找出來並呈現在畫面上:
<mat-table [dataSource]="emailsDataSource">
<ng-container matColumnDef="user">
<mat-header-cell *matHeaderCellDef>寄件人</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.user.login }}</mat-cell>
</ng-container>
...
<mat-header-row *matHeaderRowDef="['user', 'title', 'created_at', 'management']"></mat-header-row>
</mat-table>
這時候我們已經可以看到標題列的呈現囉:
有了標題列之後,我們再來把資料列本身顯示出來,透過<mat-row *matRowDef="">
的語法,我們可以決定要顯示的欄位資料,使用方式如下:
<mat-row *matRowDef="let emailRow; columns: ['user', 'title', 'created_at', 'management']"></mat-row>
在這邊我們拆成兩個部分
let emailRow
:代表每一列的資料列名稱,可以想像成類似<ng-container *ngFor="let emailRow of dataSource.data"></ng-container>
的概念。columns
:代表實際上要呈現的資料欄位名稱。這時候畫面就可以正確呈現data table資料啦!
用文字其實稍微有點難描述,畢竟這跟我們一般使用<table>
、<tr>
和<td>
的習慣不太相同,所以筆者把程式碼截圖後,搭配圖示解說,可以把以下圖片與上面的步驟做對應,應該會比較好理解:
以上就是基本的data table資料的呈現方式,剛開始可能會不太適應,但用習慣之後,你會發現這種設計其實更加直覺,維護起來也會更加容易哩!
雖然是以表格的方式呈現,但在Angular Material的data table已經把資料拆成一塊一塊的,再透過flexbox排版組合,因此已經不像原生的HTML的
<table>
了,也同時代表我們能夠有彈性的調整data table的呈現方式囉。
預設情況下,每個欄位的寬度都會被平均分配,但這不一定是我們需要的,已上述例子來說,「寄件人」其實不用那麼寬,我們可以把空間省下來,至於該如何做呢?其實在每個matColumnDef
區塊內,都會依照我們給的名稱,加上mat-column-xxxx
的class,例如寄件人欄位我們定義為matColumnDef="user"
,此時在裡面的<mat-header-cell>
和<mat-cell>
都會加上一個mat-column-user
的class,所以我們只需要定義這個class就可以啦!
.mat-column-user {
max-width: 100px;
}
.mat-header-cell.mat-column-user {
color: red;
}
.mat-cell.mat-column-user {
font-weight: bold;
text-decoration: underline dashed #000;
cursor: pointer;
}
上述CSS中我們將所有包含.mat-column-user
的cell最大寬度都設為100px
,同時我們也設定了標題cell改用紅色文字,以及內容cell的樣式調整,結果是否如我們預期呢?
成果如下:
果然完全照著我們的預期顯示,完全不用擔心跟原來習慣不同後會不會產生難以做細部調整的問題,真是太棒了!
接下來我們要加入另一個data table應該具有的重要功能,分頁。
我們可以使用<mat-paginator>
元件,立刻產生一個基本的分頁畫面,並把這個<mat-paginator>
傳給我們的MatTableDataSource
,這時候就可以把分頁功能和data table綁在一起啦!
<mat-paginator #paginator
[length]="totalCount"
[pageIndex]="0"
[pageSize]="10"
[pageSizeOptions]="[5, 10, 15]">
</mat-paginator>
<mat-paginator>
的幾個重要屬性說明如下:
length
:資料的總筆數,有這個筆數才能夠搭配其他參數算出總共有幾頁等資訊pageIndex
:目前的頁碼,從0開始,預設值是0
pageSize
:每頁要呈現幾筆資料,預設值是50
pageSizeOptions
:允許切換的每頁資料筆數接著我們調整一下程式的部分:
@Component({ })
export class EmailListComponent implements OnInit {
@ViewChild('paginator') paginator: MatPaginator;
...
ngOnInit() {
this.httpClient.get<any>('https://api.github.com/search/issues?q=repo:angular/material2&page=1').subscribe(data => {
this.totalCount = data.items.length;
this.emailsDataSource.data = data.items;
this.emailsDataSource.paginator = this.paginator;
});
}
這裡主要是設定總筆數跟data source所使用的paginator。成果如下:
剛剛我們已經完成一個基本的分頁了,不過目前這個分頁有點問題,因為分頁的對象是已經撈到前端的資料,因此撈出30筆,就只能針對這30筆做分頁,但在實務上我們常常是需要將分頁資訊傳遞給後端,由後端依照分頁資訊撈取資料後,再傳給前端顯示,這應該怎麼做呢?
這時候我們就不再需要把<mat-paginator>
指定給data srouce,而是接收<mat-paginator>
的page: Observable<PageEvent>
變動,當使用者切換分頁資訊時,再依照分頁資訊傳遞給後端重新撈取資料,而使用者切換分頁時會得到一個PageEvent,這個PageEvent有3個參數:
pageIndex
:頁碼pageSize
:每頁筆數length
:目前資料總筆數(通常是用不到)接著就讓我們來調整一下程式碼,讓分頁時可以到後端讀取資料吧!
@Component({ })
export class EmailListComponent implements OnInit {
@ViewChild('paginator') paginator: MatPaginator;
...
ngOnInit() {
this.getIssues(0, 10);
// 分頁切換時,重新取得資料
this.paginator.page.subscribe((page: PageEvent) => {
this.getIssues(page.pageIndex, page.pageSize);
});
}
getIssues(pageIndex, pageSize) {
this.httpClient
.get<any>(`https://api.github.com/search/issues?q=repo:angular/material2&page=${pageIndex + 1}&per_page=${pageSize}`)
.subscribe(data => {
this.totalCount = data.total_count;
this.emailsDataSource.data = data.items;
// 從後端取得資料時,就不用指定data srouce的paginator了
// this.emailsDataSource.paginator = this.paginator;
});
}
成果如下:
我們打開開發人員工具(F12),可以看到每次分頁切換時,就會自動往後端查詢資料,然後再更新到畫面上囉。
有了分頁後,我們再來加入排序的功能。
要替欄位加上排序功能很簡單,首先在<mat-table>
中加入matSort
,接著在要排序欄位的<mat-header-cell>
加入mat-sort-header
這個directive即可。
<mat-table [dataSource]="emailsDataSource" matSort #sortTable="matSort">
...
<ng-container matColumnDef="created_at">
<mat-header-cell *matHeaderCellDef mat-sort-header>日期</mat-header-cell>
<mat-cell *matCellDef="let row">{{ row.created_at }}</mat-cell>
</ng-container>
...
</mat-table>
跟前端資料分頁時一樣,要把這個matSort
的來源交給data source
@Component({ })
export class EmailListComponent implements OnInit {
@ViewChild('sortTable') sortTable: MatSort;
...
getIssues(pageIndex, pageSize) {
this.httpClient
.get<any>(`https://api.github.com/search/issues?q=repo:angular/material2&page=${pageIndex + 1}&per_page=${pageSize}`)
.subscribe(data => {
this.totalCount = data.total_count;
this.emailsDataSource.data = data.items;
// 設定使用前端資料排序
this.emailsDataSource.sort = this.sortTable;
// 從後端取得資料時,就不用指定data srouce的paginator了
// this.emailsDataSource.paginator = this.paginator;
});
}
結果如下:
當然,我們也一樣可以透過把資料傳遞給後端的方式來進行排序,只需要在加入matSort
的來源(也就是<mat-table>
)加入一個matSortChange
事件即可,當使用者按下欄位排序時,會傳入一個Sort
型別的變數,包含兩個欄位:
asc
,desc
和空字串
,代表要如何進行排序。<mat-table [dataSource]="emailsDataSource" matSort #sortTable="matSort" (matSortChange)="changeSort($event)">
</mat-table>
接著我們針對matSortChange
來處理排序,並把程式做一些比較大的調整:
@Component({ })
export class EmailListComponent implements OnInit {
@ViewChild('paginator') paginator: MatPaginator;
@ViewChild('sortTable') sortTable: MatSort;
currentPage: PageEvent;
currentSort: Sort;
...
ngOnInit() {
this.currentPage = {
pageIndex: 0,
pageSize: 10,
length: null
};
this.currentSort = {
active: '',
direction: ''
};
this.getIssuees();
// 分頁切換時,重新取得資料
this.paginator.page.subscribe((page: PageEvent) => {
this.currentPage = page;
this.getIssuees();
});
}
changeSort(sortInfo: Sort) {
// 因為API排序欄位是created,因此在這邊做調整
if (sortInfo.active === 'created_at') {
sortInfo.active = 'created';
}
this.currentSort = sortInfo;
this.getIssuees();
}
getIssues() {
const baseUrl = 'https://api.github.com/search/issues?q=repo:angular/material2';
let targetUrl = `${baseUrl}&page=${this.currentPage.pageIndex + 1}&per_page=${this.currentPage.pageSize}`;
if (this.currentSort.direction) {
targetUrl = `${targetUrl}&&sort=${this.currentSort.active}&order=${this.currentSort.direction}`;
}
this.httpClient
.get<any>(targetUrl)
.subscribe(data => {
this.totalCount = data.total_count;
this.emailsDataSource.data = data.items;
// 從後端進行排序時,不用指定sort
// this.emailsDataSource.sort = this.sortTable;
// 從後端取得資料時,就不用指定data srouce的paginator了
// this.emailsDataSource.paginator = this.paginator;
});
}
}
程式碼看起來有點多,但主要的部分就是把分頁和排序都串在一起,然後再一次跟後端取得資料。
成果如下:
同樣的打開開發人員工具(F12),可已看到每次按排序時,就會自動向後端抓資料囉。
在資料呈現上,data table可以說是最實用的一種呈現方式,傳統使用<table>
呈現資料有許多的限制,但若要自己使用CSS排版來重新設計一個data table也不是一件容易的事情,好在Angular Material幫我們設計了一個<mat-table>
來實現各種data table所需要的功能。
今天我們把一個data table最關鍵的三個部分:顯示資料、分頁及排序都介紹過了,剛開始也許會對於這樣的設計方式不太習慣,但多做幾次後就不難發現這種設計方式會更加直覺,管理上也更加容易。
不管是從前端整理資料,還是把排序、分頁等資訊都交給後端處理,在Angular Material的data table都有對應的方法,以期漂亮的UI,在設計上可以說是沒死角!
本日的程式碼GitHub:https://github.com/wellwind/it-ironman-demo-angular-material/tree/day-23-data-table
分支:day-23-table
明天我們會在介紹兩個進階的data table功能,以及針對排序、分頁功能做一些進階的說明。
您好:
請問您,如果是要使用"前端資料"的方法,不管是分頁、排序或搜尋,
如果table中有按鈕可以跳轉頁面(例如進入明細),
那我要如何在返回時還能保留原來table中分頁、排序、搜尋的狀態呢?
您好,建議可以將搜尋結果或搜尋條件暫存起來,如 localstorage 或記憶體內,在跳轉回來時,檢查是否有暫存的內容,有的話就顯示,沒有的話就重新查詢 :)
謝謝MIKE哥的回覆!!
另外請問您,用您的方法,我返回時pageIndex和pageSize都能取回正確的設定,但如果我加上了filter的條件,然後在ngOnInit()中加入
this.memberDataSource.filter = localstorage.getItem('filter')
顯示會幫我直接產生搜尋結果,但是資料的總筆數卻是原始筆數而不是搜尋後的筆數,不知道我該在哪修改?
謝謝您!!!
可以用this.length = this.memberDataSource.filteredData.length;
已解決,感謝您!
您好:
依照您的方法實作
但是他一直顯示
ERROR TypeError: Cannot read property 'page' of undefined
看起來是抓不到分分頁的元件,但正常是可以的,你可以把程式貼到 stackblitz 上嗎?比較好確認程式哪裡有問題
我也有遇到一樣的問題,上網查詢了可能的原因有可能為當下在ngOninit時pagination的元件還沒創出來導致subscribe失敗
因此參考網路上的解法有以下幾種
1.把對pagination的subscribe動作從ngOninit移到ngAfterViewInit
ngAfterViewInit() {
this.paginator.page.subscribe((page: PageEvent) =>
{
this.getIssues(page.pageIndex, page.pageSize);
});
}
2.修改html,在pagination的tag裡加上page的觸發事件,直接呼叫原本會呼叫的凾式(page)="getIssues($event.pageIndex, $event.pageSize)"
Mike哥您好:
請問您,如下程式碼
lineDataSource = new MatTableDataSource();
....
this.lineDataSource.data = this.onLineList;
我持續刷新this.lineDataSource.data的內容(大約一秒刷新一次),
那頁面中如果有卷軸的話,我不管卷軸往上滾還是往下滾,頁面中都會往下再多滾一次。
請問您有遇過這個問題嗎?
我沒有遇過這種問題耶,你可以把程式貼到 stackblitz 上,比較方便確認問題喔
請問可不可以把"https://api.github.com/search/issues?q=repo:angular/material2&page=1" 的JSON DATA 放在github裏分享出來?
放便去學習angular material table。
因為開啟以上這個連結顯示:
{
"message": "Validation Failed",
"errors": [
{
"message": "The listed users and repositories cannot be searched either because the resources do not exist or you do not have permission to view them.",
"resource": "Search",
"field": "q",
"code": "invalid"
}
],
"documentation_url": "https://developer.github.com/v3/search/"
}
Thank you very much
其實這網址只是 GitHub 上專案的 issue 清單,基本上都是公開的,只要換承認一個 repository 名稱都可以看到完整的內容 (當然錢題是有建立 issus)
只是 Angular 之前把 repository 名稱換掉了,所以才會出錯
現在從 angular/material2
變成了 angular/components
所以改用以下網址就可以囉
https://api.github.com/search/issues?q=repo:angular/components&page=1
請問一下 為甚麼我抓得這個Json會放不進去DataSource呢??
這樣描述太模糊了,可以簡單將你的程式發佈到 stackblitz 上以便確認嗎?